(function (window) {
    "use strict";

    var _w = window;

    var _ = {
        isObject: function (o) {
            return (o && typeof o === 'object' && !this.isArray(o));
        },
        isArray: function (o) {
            return Array.isArray(o);
        },
        clone: function (destination) {
            return this.mixin({}, destination);
        },
        mixin: function (destination, source) {
            var target = destination || {};
            for (var prop in source) {
                if (!source.hasOwnProperty(prop)) {
                    continue;
                }
                if (this.isObject(source[prop])) {
                    target[prop] = this.mixin(target[prop], source[prop]);
                } else if (this.isArray(source[prop])) {
                    target[prop] = source[prop].concat();
                } else {
                    target[prop] = source[prop];
                }
            }
            return target;
        },
        trim: function (s) {
            return s.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
        },
        getMatches: function (s, regex, index) {
            var i = (index === undefined) ? 1 : index; // default to the first capturing group
            var matches = [], match;
            while (match = regex.exec(s)) {
                matches.push(match[i]);
            }
            return matches;
        }
    };

    var reg = {
        mustNot: /(-"[^"]+")|(-[^"^\s]+)/g,
        doubleQuotesTexts: /(-*"[^"]+)"/g
    };

    var baseSearchBody = {
        from: 0,
        size: 10,
        sort: [
            "_score"
        ],
        _source: {
            includes: [],
            excludes: []
        },
        query: {
            bool: {
                must_not: [],
                must: []
            }
        },
        highlight: {
            fields: {
                "*": {
                    "fragment_size": 100,
                    "number_of_fragments": 3,
                    "fragmenter": "simple"
                }
            },
            require_field_match: false
        }
    };

    var removeMust = function (prop, type) {
        var _self = this;
        if (_self.bool.must.length === 0) {
            return;
        }
        loop: for (var i = 0, l = _self.bool.must.length; i !== l; i++) {
            var m = _self.bool.must[i];
            for (var property in m) {
                if (!m.hasOwnProperty(property) || type !== property) {
                    continue;
                }
                for (var p in m[property]) {
                    if (p === prop) {
                        _self.bool.must.splice(i, 1);
                        break loop;
                    }
                }
            }
        }
    };

    var escapeDoubleQuotes = function (s) {
        return s.replace(/([^\\])"/g, '$1\\"').replace(/^"/, '\\"');
    };

    var generateMatchs = function (prop, query, isPhrase, operator) {
        var a = [];
        if (!query) {
            return a;
        }
        var q = [];
        if (Array.isArray(query)) {
            if (isPhrase === true) {
                q = q.concat(query);
            } else {
                q.push(query.join(' '));
            }
        } else {
            q.push(String(query));
        }
        for (var i = 0, l = q.length; i !== l; i++) {
            var mo = {}, o = {};
            o[prop] = {query: q[i]};
            if (isPhrase === false && operator) {
                o[prop]['operator'] = operator;
            }
            mo[isPhrase ? 'match_phrase' : 'match'] = o;
            a.push(mo);
        }
        return a;
    };

    var parseSearchText = function (s) {
        var must = [], mustNots = [];
        if (s) {
            var dqs = _.getMatches(s, reg.doubleQuotesTexts, 0);
            var a = [];
            if (dqs.length > 0) {
                s = s.replace(reg.doubleQuotesTexts, '');// remove from origin text
            }
            var qs = _.getMatches(s, /(-*[^\s]+)/g, 0);
            a = a.concat(dqs, qs);

            if (a.length > 0) {
                for (var i = 0, l = a.length; i !== l; i++) {
                    var q = a[i];
                    if (/^-/.test(q)) {//mustNot
                        mustNots.push(escapeDoubleQuotes(q.replace(/^-*/g, '')));
                    } else {//must
                        must.push(escapeDoubleQuotes(q));
                    }
                }
            }
        }
        return {
            must: must,
            mustNot: mustNots
        };
    };

    function QueryBody(opts) {
        this.searchBody = opts.searchBody;
        this.bool = this.searchBody.query.bool;
    }

    _.mixin(QueryBody.prototype, {
        /**
         * 设置查询范围
         *
         * @param prop 范围字段名
         * @param gte 大于等于
         * @param lte 小于等于
         * @returns {QueryBody}
         */
        setRange: function (prop, gte, lte) {
            removeMust.call(this, prop, "range");
            if (!gte && !lte) {
                return;
            }
            var o = {}, range = {};
            if (gte) {
                range['gte'] = gte;
            }
            if (lte) {
                range['lte'] = lte;
            }
            o[prop] = range;
            this.bool.must.push({
                "range": o
            });
            return this;
        },
        /**
         *
         * 设置必须检索的条件
         *
         * @param prop 检索的字段名
         * @param query 查询的字段
         * @param isPhrase 是否词组匹配
         * @param operator 逻辑符，and和or。注意isPhrase模式下，无效。
         * @returns {QueryBody}
         */
        addMust: function (prop, query, isPhrase, operator) {
            var a = generateMatchs(prop, query, isPhrase, operator);
            this.bool.must = this.bool.must.concat(a);
            return this;
        },
        /**
         *
         * 设置"不"必须检索的条件
         *
         * @param prop 检索的字段名
         * @param query 查询的字段
         * @param isPhrase 是否词组匹配
         * @param operator 逻辑符，and和or。注意isPhrase模式下，无效。
         * @returns {QueryBody}
         */
        addMustNot: function (prop, query, isPhrase, operator) {
            var a = generateMatchs(prop, query, isPhrase, operator);
            this.bool.must_not = this.bool.must_not.concat(a);
            return this;
        },
        /**
         *
         * 返回json对象
         *
         * @returns {*}
         */
        toJSON: function () {
            return this.searchBody;
        },
        /**
         *
         * 设置检索的文档类型。
         *
         * @param types 数组
         * @returns {QueryBody}
         */
        setTypes: function (types) {
            if (!_.isArray(types)) {
                alert('Types must be Array');
                return this;
            }
            removeMust.call(this, "type", "match");
            if (types && types.length > 0) {
                this.addMust("type", types.join(" "), false, "or");
            }
            return this;
        },
        /**
         *
         * 设置检索的起始条目。
         *
         * @param index
         * @returns {QueryBody}
         */
        setStartIndex: function (index) {
            this.searchBody.from = index;
            return this;
        },
        /**
         *
         * 设置检索的分页大小。
         *
         * @param size
         * @returns {QueryBody}
         */
        setPageSize: function (size) {
            this.searchBody.size = size;
            return this;
        },
        /**
         *
         * 设置查询时，过滤的source属性。
         *
         * @param arr 数组
         * @returns {QueryBody}
         */
        setExcludesProps: function (arr) {
            if (!_.isArray(arr)) {
                alert('ExcludesProps must be Array');
                return this;
            }
            this.searchBody._source.excludes = arr;
            return this;
        },
        /**
         * 返回JSON的字符串
         *
         * @returns String
         */
        toString: function () {
            return JSON.stringify(this.toJSON());
        }
    });

    var ElasticSearch = function (opts) {
        // options
        this.options = _.clone(this.constructor.defaults);
        this.option(opts);
    };

    ElasticSearch.defaults = {
        searchBody: baseSearchBody,
        isPhrase: true
    };

    _.mixin(ElasticSearch.prototype, {
        option: function (options) {
            _.mixin(this.options, options);
        },
        /**
         * 构建查询体对象
         *
         * @param text
         * @param types
         * @returns QueryBody
         */
        getQueryBody: function (text, types) {
            text = _.trim(text);
            var qo = parseSearchText(text.concat());
            var q = new QueryBody(this.options)
                .addMust("_all", qo.must, this.options.isPhrase, "and");
            if (qo.mustNot && qo.mustNot.length > 0) {
                q.addMustNot("_all", qo.mustNot, this.options.isPhrase, "and");
            }
            if (types) {
                q.setTypes(types);
            }
            return q;
        }
    });

    _w.ElasticSearch = ElasticSearch;

})(window);